{
    "cells": [
        {
            "cell_type": "markdown",
            "metadata": {},
            "source": [
                "<i>Copyright (c) Recommenders contributors.</i>\n",
                "\n",
                "<i>Licensed under the MIT License.</i>"
            ]
        },
        {
            "cell_type": "markdown",
            "metadata": {},
            "source": [
                "# Neural Collaborative Filtering on MovieLens dataset.\n",
                "\n",
                "Neural Collaborative Filtering (NCF) is a well known recommendation algorithm that generalizes the matrix factorization problem with multi-layer perceptron. \n",
                "\n",
                "This notebook provides an example of how to utilize and evaluate NCF implementation in the `recommenders`. We use a smaller dataset in this example to run NCF efficiently with GPU acceleration on a [Data Science Virtual Machine](https://azure.microsoft.com/en-gb/services/virtual-machines/data-science-virtual-machines/)."
            ]
        },
        {
            "cell_type": "code",
            "execution_count": 1,
            "metadata": {},
            "outputs": [],
            "source": [
                "%load_ext autoreload\n",
                "%autoreload 2"
            ]
        },
        {
            "cell_type": "code",
            "execution_count": 3,
            "metadata": {},
            "outputs": [
                {
                    "name": "stdout",
                    "output_type": "stream",
                    "text": [
                        "System version: 3.7.5 (default, Dec  9 2021, 17:04:37) \n",
                        "[GCC 8.4.0]\n",
                        "Pandas version: 1.3.5\n",
                        "Tensorflow version: 2.7.0\n"
                    ]
                }
            ],
            "source": [
                "import sys\n",
                "import pandas as pd\n",
                "import tensorflow as tf\n",
                "tf.get_logger().setLevel('ERROR') # only show error messages\n",
                "\n",
                "from recommenders.utils.timer import Timer\n",
                "from recommenders.models.ncf.ncf_singlenode import NCF\n",
                "from recommenders.models.ncf.dataset import Dataset as NCFDataset\n",
                "from recommenders.datasets import movielens\n",
                "from recommenders.datasets.python_splitters import python_chrono_split\n",
                "from recommenders.evaluation.python_evaluation import (\n",
                "    map, ndcg_at_k, precision_at_k, recall_at_k\n",
                ")\n",
                "from recommenders.utils.notebook_utils import store_metadata\n",
                "\n",
                "print(\"System version: {}\".format(sys.version))\n",
                "print(\"Pandas version: {}\".format(pd.__version__))\n",
                "print(\"Tensorflow version: {}\".format(tf.__version__))"
            ]
        },
        {
            "cell_type": "markdown",
            "metadata": {},
            "source": [
                "Set the default parameters."
            ]
        },
        {
            "cell_type": "code",
            "execution_count": 4,
            "metadata": {
                "tags": [
                    "parameters"
                ]
            },
            "outputs": [],
            "source": [
                "# top k items to recommend\n",
                "TOP_K = 10\n",
                "\n",
                "# Select MovieLens data size: 100k, 1m, 10m, or 20m\n",
                "MOVIELENS_DATA_SIZE = '100k'\n",
                "\n",
                "# Model parameters\n",
                "EPOCHS = 50\n",
                "BATCH_SIZE = 256\n",
                "\n",
                "SEED = 42"
            ]
        },
        {
            "cell_type": "markdown",
            "metadata": {},
            "source": [
                "### 1. Download the MovieLens dataset"
            ]
        },
        {
            "cell_type": "code",
            "execution_count": 5,
            "metadata": {},
            "outputs": [
                {
                    "name": "stderr",
                    "output_type": "stream",
                    "text": [
                        "INFO:recommenders.datasets.download_utils:Downloading https://files.grouplens.org/datasets/movielens/ml-100k.zip\n",
                        "100%|██████████| 4.81k/4.81k [00:00<00:00, 16.9kKB/s]\n"
                    ]
                }
            ],
            "source": [
                "df = movielens.load_pandas_df(\n",
                "    size=MOVIELENS_DATA_SIZE,\n",
                "    header=[\"userID\", \"itemID\", \"rating\", \"timestamp\"]\n",
                ")"
            ]
        },
        {
            "cell_type": "markdown",
            "metadata": {},
            "source": [
                "### 2. Split the data using the Spark chronological splitter provided in utilities"
            ]
        },
        {
            "cell_type": "code",
            "execution_count": 6,
            "metadata": {},
            "outputs": [],
            "source": [
                "train, test = python_chrono_split(df, 0.75)"
            ]
        },
        {
            "cell_type": "markdown",
            "metadata": {},
            "source": [
                "Filter out any users or items in the test set that do not appear in the training set."
            ]
        },
        {
            "cell_type": "code",
            "execution_count": 7,
            "metadata": {},
            "outputs": [],
            "source": [
                "test = test[test[\"userID\"].isin(train[\"userID\"].unique())]\n",
                "test = test[test[\"itemID\"].isin(train[\"itemID\"].unique())]"
            ]
        },
        {
            "cell_type": "markdown",
            "metadata": {},
            "source": [
                "Write datasets to csv files."
            ]
        },
        {
            "cell_type": "code",
            "execution_count": 8,
            "metadata": {},
            "outputs": [],
            "source": [
                "train_file = \"./train.csv\"\n",
                "test_file = \"./test.csv\"\n",
                "train.to_csv(train_file, index=False)\n",
                "test.to_csv(test_file, index=False)"
            ]
        },
        {
            "cell_type": "markdown",
            "metadata": {},
            "source": [
                "Generate an NCF dataset object from the data subsets."
            ]
        },
        {
            "cell_type": "code",
            "execution_count": 9,
            "metadata": {},
            "outputs": [
                {
                    "name": "stderr",
                    "output_type": "stream",
                    "text": [
                        "INFO:recommenders.models.ncf.dataset:Indexing ./train.csv ...\n",
                        "INFO:recommenders.models.ncf.dataset:Indexing ./test.csv ...\n",
                        "INFO:recommenders.models.ncf.dataset:Creating full leave-one-out test file ./test_full.csv ...\n",
                        "100%|██████████| 943/943 [00:28<00:00, 33.06it/s]\n",
                        "INFO:recommenders.models.ncf.dataset:Indexing ./test_full.csv ...\n"
                    ]
                }
            ],
            "source": [
                "data = NCFDataset(train_file=train_file, test_file=test_file, seed=SEED)"
            ]
        },
        {
            "cell_type": "markdown",
            "metadata": {},
            "source": [
                "### 3. Train the NCF model on the training data, and get the top-k recommendations for our testing data\n",
                "\n",
                "NCF accepts implicit feedback and generates prospensity of items to be recommended to users in the scale of 0 to 1. A recommended item list can then be generated based on the scores. Note that this quickstart notebook is using a smaller number of epochs to reduce time for training. As a consequence, the model performance will be slighlty deteriorated. "
            ]
        },
        {
            "cell_type": "code",
            "execution_count": null,
            "metadata": {},
            "outputs": [],
            "source": [
                "model = NCF (\n",
                "    n_users=data.n_users, \n",
                "    n_items=data.n_items,\n",
                "    model_type=\"NeuMF\",\n",
                "    n_factors=4,\n",
                "    layer_sizes=[16,8,4],\n",
                "    n_epochs=EPOCHS,\n",
                "    batch_size=BATCH_SIZE,\n",
                "    learning_rate=1e-3,\n",
                "    verbose=10,\n",
                "    seed=SEED\n",
                ")"
            ]
        },
        {
            "cell_type": "code",
            "execution_count": 11,
            "metadata": {},
            "outputs": [
                {
                    "name": "stderr",
                    "output_type": "stream",
                    "text": [
                        "INFO:recommenders.models.ncf.ncf_singlenode:Epoch 10 [6.31s]: train_loss = 0.259318 \n",
                        "INFO:recommenders.models.ncf.ncf_singlenode:Epoch 20 [6.28s]: train_loss = 0.246134 \n",
                        "INFO:recommenders.models.ncf.ncf_singlenode:Epoch 30 [6.21s]: train_loss = 0.240125 \n",
                        "INFO:recommenders.models.ncf.ncf_singlenode:Epoch 40 [6.23s]: train_loss = 0.235913 \n",
                        "INFO:recommenders.models.ncf.ncf_singlenode:Epoch 50 [6.31s]: train_loss = 0.232268 \n"
                    ]
                },
                {
                    "name": "stdout",
                    "output_type": "stream",
                    "text": [
                        "Took 317.7864 seconds for training.\n"
                    ]
                }
            ],
            "source": [
                "with Timer() as train_time:\n",
                "    model.fit(data)\n",
                "\n",
                "print(\"Took {} seconds for training.\".format(train_time))"
            ]
        },
        {
            "cell_type": "markdown",
            "metadata": {},
            "source": [
                "In the movie recommendation use case scenario, seen movies are not recommended to the users."
            ]
        },
        {
            "cell_type": "code",
            "execution_count": 12,
            "metadata": {},
            "outputs": [
                {
                    "name": "stdout",
                    "output_type": "stream",
                    "text": [
                        "Took 2.7835 seconds for prediction.\n"
                    ]
                }
            ],
            "source": [
                "with Timer() as test_time:\n",
                "    users, items, preds = [], [], []\n",
                "    item = list(train.itemID.unique())\n",
                "    for user in train.userID.unique():\n",
                "        user = [user] * len(item) \n",
                "        users.extend(user)\n",
                "        items.extend(item)\n",
                "        preds.extend(list(model.predict(user, item, is_list=True)))\n",
                "\n",
                "    all_predictions = pd.DataFrame(data={\"userID\": users, \"itemID\":items, \"prediction\":preds})\n",
                "\n",
                "    merged = pd.merge(train, all_predictions, on=[\"userID\", \"itemID\"], how=\"outer\")\n",
                "    all_predictions = merged[merged.rating.isnull()].drop('rating', axis=1)\n",
                "\n",
                "print(\"Took {} seconds for prediction.\".format(test_time))"
            ]
        },
        {
            "cell_type": "markdown",
            "metadata": {},
            "source": [
                "### 4. Evaluate how well NCF performs"
            ]
        },
        {
            "cell_type": "markdown",
            "metadata": {},
            "source": [
                "The ranking metrics are used for evaluation."
            ]
        },
        {
            "cell_type": "code",
            "execution_count": 13,
            "metadata": {},
            "outputs": [
                {
                    "name": "stdout",
                    "output_type": "stream",
                    "text": [
                        "MAP:\t0.049650\n",
                        "NDCG:\t0.200524\n",
                        "Precision@K:\t0.183033\n",
                        "Recall@K:\t0.102721\n"
                    ]
                }
            ],
            "source": [
                "eval_map = map(test, all_predictions, col_prediction='prediction', k=TOP_K)\n",
                "eval_ndcg = ndcg_at_k(test, all_predictions, col_prediction='prediction', k=TOP_K)\n",
                "eval_precision = precision_at_k(test, all_predictions, col_prediction='prediction', k=TOP_K)\n",
                "eval_recall = recall_at_k(test, all_predictions, col_prediction='prediction', k=TOP_K)\n",
                "\n",
                "print(\"MAP:\\t%f\" % eval_map,\n",
                "      \"NDCG:\\t%f\" % eval_ndcg,\n",
                "      \"Precision@K:\\t%f\" % eval_precision,\n",
                "      \"Recall@K:\\t%f\" % eval_recall, sep='\\n')"
            ]
        },
        {
            "cell_type": "code",
            "execution_count": null,
            "metadata": {},
            "outputs": [],
            "source": [
                "# Record results for tests - ignore this cell\n",
                "store_metadata(\"map\", eval_map)\n",
                "store_metadata(\"ndcg\", eval_ndcg)\n",
                "store_metadata(\"precision\", eval_precision)\n",
                "store_metadata(\"recall\", eval_recall)\n",
                "store_metadata(\"train_time\", train_time.interval)\n",
                "store_metadata(\"test_time\", test_time.interval)"
            ]
        },
        {
            "cell_type": "code",
            "execution_count": null,
            "metadata": {},
            "outputs": [],
            "source": []
        }
    ],
    "metadata": {
        "celltoolbar": "Tags",
        "kernelspec": {
            "display_name": "reco_gpu",
            "language": "python",
            "name": "conda-env-reco_gpu-py"
        },
        "language_info": {
            "codemirror_mode": {
                "name": "ipython",
                "version": 3
            },
            "file_extension": ".py",
            "mimetype": "text/x-python",
            "name": "python",
            "nbconvert_exporter": "python",
            "pygments_lexer": "ipython3",
            "version": "3.7.11"
        }
    },
    "nbformat": 4,
    "nbformat_minor": 4
}
